﻿using System;
using System.IO;
using System.Security.Cryptography;
using EncryptionLib.Users;
using EncryptionLib.Util;
using Ionic.Zip;
using System.Diagnostics;
using System.Windows.Forms;
using System.Threading;

namespace EncryptionLib.Encryption
{
    public class Decryptor : IDecryptor
    {
        protected class DecryptThreadParam
        {
            public Control Context;
            public byte[] Passphrase;

            public DecryptThreadParam(Control context, byte[] passphrase)
            {
                Context = context;
                Passphrase = passphrase;
            }
        }

        public const string DECRYPTOR_VERSION = "1.0";

        public event EncryptionProgressHandler DecryptAsyncProgress;

        public UserData Receiver
        {
            get;
            set;
        }

        /// <summary>
        /// full path to the encrypted file (to be decrypted).
        /// </summary>
        public string EncryptedFile
        {
            get;set;
        }

        public string OutputPath
        {
            get;
            set;
        }

        public void Decrypt(string sPassphrase)
        {
            Decrypt(SerializerHelper.String2Bytes(sPassphrase));
        }

        public void Decrypt(byte[] arrPassphrase)
        {
            DoDecrypt(arrPassphrase, null, false);
        }

        public void DecryptAsync(Control context, string sPassphrase)
        {
            DecryptAsync(context, SerializerHelper.String2Bytes(sPassphrase));
        }

        public void DecryptAsync(Control context, byte[] arrPassphrase)
        {
            Thread thDecrypt = new Thread(new ParameterizedThreadStart(this.DecryptThread));
            thDecrypt.IsBackground = true;
            thDecrypt.Start(new DecryptThreadParam(context, arrPassphrase));
        }

        #region Utilities

        protected static int CompareVersion(string sVer1, string sVer2)
        {
            string[] arrVer1 = sVer1.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
            string[] arrVer2 = sVer2.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries);

            Func<string[], int> calcVersion = delegate(string[] arr)
            {
                int lRet = 0;
                for (int i = 0; i < arr.Length; ++i)
                {
                    int iVal;
                    if (i < 4 && int.TryParse(arr[i], out iVal))
                        lRet += iVal * (int)Math.Pow(10, 3 - i);
                }
                return lRet;
            };
            int lVer1 = calcVersion(arrVer1);
            int lVer2 = calcVersion(arrVer2);
            return ((lVer1 < lVer2) ? -1 : (lVer1 == lVer2 ? 0 : 1));
        }

        protected void GetSecretKey(string sPrivateKey, byte[] arrEncryptedSecretKey,
            ref byte[] arrPassword, ref byte[] arrIV)
        {
            RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
            rsa.FromXmlString(sPrivateKey);

            MemoryStream msEncryptedSecretKey = new MemoryStream(arrEncryptedSecretKey);
            MemoryStream msPlainSecretKey = new MemoryStream();

            int BUFFER_SIZE = rsa.ExportParameters(false).Modulus.GetLength(0);

            msEncryptedSecretKey.Seek(0, SeekOrigin.Begin);
            msPlainSecretKey.Seek(0, SeekOrigin.Begin);

            while (msEncryptedSecretKey.Position < msEncryptedSecretKey.Length)
            {
                byte[] arrInData = new byte[BUFFER_SIZE];
                int iRead = msEncryptedSecretKey.Read(arrInData, 0, BUFFER_SIZE);
                byte[] arrOutData;

                if (iRead > 0 && iRead != BUFFER_SIZE)
                {
                    arrInData = (byte[])SerializerHelper.ResizeArray(arrInData, iRead);
                }

                if (iRead > 0)
                {
                    arrOutData = rsa.Decrypt(arrInData, false);
                    msPlainSecretKey.Write(arrOutData, 0, arrOutData.Length);
                }
            }
            msEncryptedSecretKey.Dispose();

            msPlainSecretKey.Seek(0, SeekOrigin.Begin);

            arrPassword = SerializerHelper.ReadBytes(msPlainSecretKey);
            arrIV = SerializerHelper.ReadBytes(msPlainSecretKey);

            msPlainSecretKey.Dispose();
        }

        protected void DESDecrypt(bool bZipped, string sOutputFile, EncryptFileHeader header, 
            Stream sSrcStream, byte[] arrPassword, byte[] arrIV)
        {
            CryptoStream cs = null;

            //backup
            if(File.Exists(sOutputFile))
            {
                bool bNotYet = true;
                int i = 0;
                string sBackupFile;
                do 
                {
                    sBackupFile = Path.Combine(Path.GetDirectoryName(sOutputFile),
                        string.Format("{0}.bak{1}", Path.GetFileName(sOutputFile), i++));
                    bNotYet = File.Exists(sBackupFile);
                } while (bNotYet);
                File.Move(sOutputFile, sBackupFile);
            }
            FileStream fsOut = new FileStream(sOutputFile, FileMode.Create, FileAccess.Write);

            try
            {
                SymmetricAlgorithm algo = SymmetricAlgo.CreateSymmetricAlgorithm(header.Algorithm);
                algo.Mode = header.EncryptCipherMode;
                algo.Padding = header.EncryptPaddingMode;
                algo.Key = arrPassword;
                algo.IV = arrIV;

                cs = new CryptoStream(sSrcStream,
                   algo.CreateDecryptor(),
                   CryptoStreamMode.Read);

                while (sSrcStream.Position < sSrcStream.Length)
                {
                    byte[] plainbytes = new byte[4096];
                    int iRead = cs.Read(plainbytes, 0, 4096);
                    fsOut.Write(plainbytes, 0, iRead);
                }
            }
            catch (System.Exception ex)
            {
                throw ex;
            }
            finally
            {
                if (cs != null)
                    cs.Close();
                if (fsOut != null)
                    fsOut.Close();
            }
        }

        protected void Unzip(string sInputFile)
        {
            using (ZipFile zip = ZipFile.Read(sInputFile))
            {
                foreach (ZipEntry e in zip)
                {
                    e.Extract(OutputPath);
                }
            }
        }

        protected void OnDecryptAsyncProgress(Control context, EncryptionProgressArgs args)
        {
            if (DecryptAsyncProgress == null)
                return;

            if(context.InvokeRequired)
            {
                context.Invoke(DecryptAsyncProgress, this, args);
            }
            else
            {
                DecryptAsyncProgress(this, args);
            }
        }

        protected void DoDecrypt(byte[] arrPassphrase, Control context, bool bNotifyProgress)
        {
            EncryptFileHeader header;
            string sOutputFile;

            if (bNotifyProgress)
                OnDecryptAsyncProgress(context, new EncryptionProgressArgs(0));

            using (FileStream fs = new FileStream(EncryptedFile, FileMode.Open, FileAccess.Read))
            {
                // read the header
                header = EncryptFileHeader.Deserialize(fs);

                if (bNotifyProgress)
                    OnDecryptAsyncProgress(context, new EncryptionProgressArgs(5));

                if (CompareVersion(header.Version, DECRYPTOR_VERSION) != 0)
                {
                    if (bNotifyProgress)
                    {
                        OnDecryptAsyncProgress(context,
                            new EncryptionProgressArgs(-1, 
                                new Exception(string.Format("Invalid version: {0}", header.Version))));
                        return;
                    }
                    else
                        throw new InvalidDataException(string.Format("Decryptor cannot decrypt data with version {0}.", header.Version));
                }

                // read the secret key
                byte[] arrPassword = null, arrIV = null;
                string sPrivateKey = Receiver.GetPlainPrivateKey(arrPassphrase);
                GetSecretKey(sPrivateKey, header.SecretKey, ref arrPassword, ref arrIV);

                if (bNotifyProgress)
                    OnDecryptAsyncProgress(context, new EncryptionProgressArgs(10));

                // decrypt
                if (header.Zipped)
                {
                    sOutputFile = Path.GetFileName(Path.GetTempFileName());
                }
                else
                {
                    sOutputFile = SerializerHelper.ReadString(fs);
                }
                sOutputFile = Path.Combine(OutputPath, sOutputFile);
                DESDecrypt(header.Zipped, sOutputFile, header, fs, arrPassword, arrIV);
                fs.Close();

                if (bNotifyProgress)
                    OnDecryptAsyncProgress(context, new EncryptionProgressArgs(50));
            }

            // unzip (if needed)
            if (header.Zipped)
            {
                Unzip(sOutputFile);
                try
                {
                    File.Delete(sOutputFile);
                }
                catch (System.Exception ex)
                {
                    Debug.WriteLine(string.Format("Error when deleting temp file: {0}",
                        ex.Message));
                }
            }
            if (bNotifyProgress)
                OnDecryptAsyncProgress(context, new EncryptionProgressArgs(100));
        }

        protected void DecryptThread(object param)
        {
            DecryptThreadParam data = (DecryptThreadParam)param;
            try
            {
                DoDecrypt(data.Passphrase, data.Context, true);
            }
            catch(Exception ex)
            {
                OnDecryptAsyncProgress(data.Context, 
                    new EncryptionProgressArgs(-1, ex));
            }
            
        }

        #endregion
    }
}
